﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Color.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

using Haplous.Core;
using Haplous.Correctness;
using Haplous.Properties;

using JetBrains.Annotations;

namespace Haplous
{
    /// <summary>
    ///     A class used to define the color of visible elements.
    /// </summary>
    [DebuggerDisplay("Color: {_alpha},{_red},{_green},{_blue}")]
    public class Color : NotifyObject, IEquatable<Color>
    {
        /// <summary>
        ///     The alpha (transparency) channel of the color - 255 indicates fully opaque and 0 indicates fully transparent.
        /// </summary>
        private byte _alpha;

        /// <summary>
        ///     The blue channel of the color.
        /// </summary>
        private byte _blue;

        /// <summary>
        ///     The green channel of the color.
        /// </summary>
        private byte _green;

        /// <summary>
        ///     The red channel of the color.
        /// </summary>
        private byte _red;

        /// <summary>
        ///     Initializes a new instance of the <see cref="Color" /> class.
        /// </summary>
        /// <param name="alpha">
        ///     The alpha (transparency) channel of the color - 255 indicates fully opaque and 0 indicates fully
        ///     transparent.
        /// </param>
        /// <param name="blue">The blue channel of the color.</param>
        /// <param name="green">The green channel of the color.</param>
        /// <param name="red">The red channel of the color.</param>
        public Color(byte alpha, byte blue, byte green, byte red)
        {
            _alpha = alpha;
            _blue = blue;
            _green = green;
            _red = red;
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="Color" /> class.
        /// </summary>
        /// <param name="blue">The blue channel of the color.</param>
        /// <param name="green">The green channel of the color.</param>
        /// <param name="red">The red channel of the color.</param>
        public Color(byte blue, byte green, byte red)
        {
            _alpha = byte.MaxValue;
            _blue = blue;
            _green = green;
            _red = red;
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="Color" /> class.
        /// </summary>
        /// <param name="value">
        ///     The value to parse for the initial color (using standard web formats: RGB, #RGB, ARGB, RRGGBB,
        ///     #RRGGBB, AARRGGBB, #AARRGGBB.).
        /// </param>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     Expected <paramref name="value" /> to be in one of the following formats: RGB, #RGB, ARGB, RRGGBB, #RRGGBB,
        ///     AARRGGBB, #AARRGGBB.
        /// </exception>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     The characters at a position in the string could not be interpreted as a hexadecimal number.
        /// </exception>
        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public Color([NotNull] string value)
        {
            Set(value);
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="Color" /> class.
        /// </summary>
        /// <remarks>The color defaults to fully opaque black.</remarks>
        public Color()
        {
            _alpha = byte.MaxValue;
            _blue = 0;
            _green = 0;
            _red = 0;
        }

        /// <summary>
        ///     The alpha (transparency) channel of the color - 255 indicates fully opaque and 0 indicates fully transparent.
        /// </summary>
        public byte Alpha
        {
            get { return _alpha; }
            set { OnPropertyChanged("Alpha", ref _alpha, value); }
        }

        /// <summary>
        ///     The red channel of the color.
        /// </summary>
        public byte Red
        {
            get { return _red; }
            set { OnPropertyChanged("Red", ref _red, value); }
        }

        /// <summary>
        ///     The green channel of the color.
        /// </summary>
        public byte Green
        {
            get { return _green; }
            set { OnPropertyChanged("Green", ref _green, value); }
        }

        /// <summary>
        ///     The blue channel of the color.
        /// </summary>
        public byte Blue
        {
            get { return _blue; }
            set { OnPropertyChanged("Blue", ref _blue, value); }
        }

        /// <summary>
        ///     Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <returns>
        ///     true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.
        /// </returns>
        /// <param name="other">An object to compare with this object.</param>
        public bool Equals([CanBeNull] Color other)
        {
            if(ReferenceEquals(null, other))
                return false;
            if(ReferenceEquals(this, other))
                return true;
            return _alpha == other._alpha && _blue == other._blue && _green == other._green && _red == other._red;
        }

        /// <summary>
        ///     Sets the specified color channels.
        /// </summary>
        /// <param name="value">
        ///     The value to parse for the initial color (using standard web formats: RGB, #RGB, ARGB, RRGGBB,
        ///     #RRGGBB, AARRGGBB, #AARRGGBB.).
        /// </param>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     Expected <paramref name="value" /> to be in one of the following formats: RGB, #RGB, ARGB, RRGGBB, #RRGGBB,
        ///     AARRGGBB, #AARRGGBB.
        /// </exception>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     The characters at a position in the string could not be interpreted as a hexadecimal number.
        /// </exception>
        public void Set([NotNull] string value)
        {
            value.ValidateArgument("value", StringMustNotBe.NullOrEmptyOrWhiteSpace);

            byte alpha, red, green, blue;

            switch(value.Length)
            {
                case 3:
                    // RGB
                    red = GetComponent(value, 0, 1, "value", "red", "Rgb");
                    green = GetComponent(value, 1, 1, "value", "green", "rGb");
                    blue = GetComponent(value, 2, 1, "value", "blue", "rgB");
                    alpha = byte.MaxValue;
                    break;

                case 4:
                    // #RGB
                    // ARGB
                    if(value[0] == '#')
                    {
                        red = GetComponent(value, 1, 1, "value", "red", "#Rgb");
                        green = GetComponent(value, 2, 1, "value", "green", "#rGb");
                        blue = GetComponent(value, 3, 1, "value", "blue", "#rgB");
                        alpha = byte.MaxValue;
                    }
                    else
                    {
                        alpha = GetComponent(value, 0, 1, "value", "alpha", "Argb");
                        red = GetComponent(value, 1, 1, "value", "red", "aRgb");
                        green = GetComponent(value, 2, 1, "value", "green", "arGb");
                        blue = GetComponent(value, 3, 1, "value", "blue", "argB");
                    }
                    break;

                case 5:
                    // #ARGB
                    if(value[0] != '#')
                        throw new ArgumentOutOfRangeException("value", value, Resources.Error_ColorStringExpectedToStartWithHashSymbol);

                    alpha = GetComponent(value, 1, 1, "value", "alpha", "#Argb");
                    red = GetComponent(value, 2, 1, "value", "red", "#aRgb");
                    green = GetComponent(value, 3, 1, "value", "green", "#arGb");
                    blue = GetComponent(value, 4, 1, "value", "blue", "#argB");
                    break;

                case 6:
                    // RRGGBB
                    red = GetComponent(value, 0, 2, "value", "red", "RRggbb");
                    green = GetComponent(value, 2, 2, "value", "green", "rrGGbb");
                    blue = GetComponent(value, 4, 2, "value", "blue", "rrggBB");
                    alpha = byte.MaxValue;
                    break;

                case 7:
                    // #RRGGBB
                    if(value[0] != '#')
                        throw new ArgumentOutOfRangeException("value", value, Resources.Error_ColorStringExpectedToStartWithHashSymbol);

                    red = GetComponent(value, 1, 2, "value", "red", "RRggbb");
                    green = GetComponent(value, 3, 2, "value", "green", "rrGGbb");
                    blue = GetComponent(value, 5, 2, "value", "blue", "rrggBB");
                    alpha = byte.MaxValue;
                    break;

                case 8:
                    // AARRGGBB
                    alpha = GetComponent(value, 0, 2, "value", "alpha", "AArrggbb");
                    red = GetComponent(value, 2, 2, "value", "red", "aaRRggbb");
                    green = GetComponent(value, 4, 2, "value", "green", "aarrGGbb");
                    blue = GetComponent(value, 6, 2, "value", "blue", "aarrggBB");
                    break;
                case 9:
                    // #AARRGGBB
                    if(value[0] != '#')
                        throw new ArgumentOutOfRangeException("value", value, Resources.Error_ColorStringExpectedToStartWithHashSymbol);

                    alpha = GetComponent(value, 1, 2, "value", "alpha", "#AArrggbb");
                    red = GetComponent(value, 3, 2, "value", "red", "#aaRRggbb");
                    green = GetComponent(value, 5, 2, "value", "green", "#aarrGGbb");
                    blue = GetComponent(value, 7, 2, "value", "blue", "#aarrggBB");
                    break;

                default:
                    throw new ArgumentOutOfRangeException("value", value, Resources.Error_ColorStringExpectedToBeInStandardFormat);
            }

            Set(alpha, red, green, blue);
        }

        /// <summary>
        ///     Sets the specified color channels.
        /// </summary>
        /// <param name="blue">The blue channel of the color.</param>
        /// <param name="green">The green channel of the color.</param>
        /// <param name="red">The red channel of the color.</param>
        public void Set(byte blue, byte green, byte red)
        {
            Set(byte.MaxValue, red, green, blue);
        }

        /// <summary>
        ///     Sets the specified color channels.
        /// </summary>
        /// <param name="alpha">
        ///     The alpha (transparency) channel of the color - 255 indicates fully opaque and 0 indicates fully
        ///     transparent.
        /// </param>
        /// <param name="blue">The blue channel of the color.</param>
        /// <param name="green">The green channel of the color.</param>
        /// <param name="red">The red channel of the color.</param>
        public void Set(byte alpha, byte blue, byte green, byte red)
        {
            var alphaChanged = (_alpha != alpha);
            _alpha = alpha;

            var redChanged = (_red != red);
            _red = red;

            var greenChanged = (_green != green);
            _green = green;

            var blueChanged = (_blue != blue);
            _blue = blue;

            if(blueChanged)
                OnPropertyChanged("Blue");

            if(greenChanged)
                OnPropertyChanged("Green");

            if(redChanged)
                OnPropertyChanged("Red");

            if(alphaChanged)
                OnPropertyChanged("Alpha");
        }

        /// <summary>
        ///     Gets the component.
        /// </summary>
        /// <param name="value">The value from which to extract a number.</param>
        /// <param name="position">The position at which to start extracting.</param>
        /// <param name="length">The length of the string to extract.</param>
        /// <param name="argumentName">The name of the argument to report in the event of an error.</param>
        /// <param name="componentName">The name of the component being extracted.</param>
        /// <param name="format">The format illustrating where the component appears.</param>
        /// <returns>A byte representing the value extracted</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     The characters at the position specified could not be interpreted
        ///     as a hexadecimal number.
        /// </exception>
        [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "componentName")]
        private static byte GetComponent([NotNull] string value, int position, int length, [NotNull] string argumentName, [NotNull] string componentName, [NotNull] string format)
        {
            var digits = value.Substring(position, length);
            byte number;
            if(!byte.TryParse(digits, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out number))
                throw new ArgumentOutOfRangeException(argumentName, value, string.Format(CultureInfo.InvariantCulture, Resources.Error_ExpectPortionOfColorToBeHexadecimal, position, length, format));

            return number;
        }

        /// <summary>
        ///     Returns a string that represents the current object.
        /// </summary>
        /// <returns>
        ///     A string that represents the current object.
        /// </returns>
        public override string ToString()
        {
            return "#" + _alpha.ToString("x2", CultureInfo.InvariantCulture) + _red.ToString("x2", CultureInfo.InvariantCulture) + _green.ToString("x2", CultureInfo.InvariantCulture) +
                   _blue.ToString("x2", CultureInfo.InvariantCulture);
        }

        /// <summary>
        ///     Determines whether the specified <see cref="T:System.Object" /> is equal to the current
        ///     <see cref="T:System.Object" />.
        /// </summary>
        /// <returns>
        ///     true if the specified <see cref="T:System.Object" /> is equal to the current <see cref="T:System.Object" />;
        ///     otherwise, false.
        /// </returns>
        /// <param name="obj">The object to compare with the current object. </param>
        public override bool Equals([CanBeNull] object obj)
        {
            if(ReferenceEquals(null, obj))
                return false;
            if(ReferenceEquals(this, obj))
                return true;
            if(obj.GetType() != GetType())
                return false;
            return Equals((Color)obj);
        }

        /// <summary>
        ///     Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        ///     A hash code for the current <see cref="T:Color" />.
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = _alpha.GetHashCode();
                hashCode = (hashCode * 397) ^ _blue.GetHashCode();
                hashCode = (hashCode * 397) ^ _green.GetHashCode();
                hashCode = (hashCode * 397) ^ _red.GetHashCode();
                return hashCode;
            }
        }

        /// <summary>
        ///     Implements the equality operator.
        /// </summary>
        /// <param name="left">The left operand.</param>
        /// <param name="right">The right operand.</param>
        /// <returns>The result of the operation.</returns>
        public static bool operator ==(Color left, Color right)
        {
            return Equals(left, right);
        }

        /// <summary>
        ///     Implements the inequality operator.
        /// </summary>
        /// <param name="left">The left operand.</param>
        /// <param name="right">The right operand.</param>
        /// <returns>The result of the operation.</returns>
        public static bool operator !=(Color left, Color right)
        {
            return !Equals(left, right);
        }
    }
}